package satir.framework;

import java.util.Hashtable;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import satir.framework.JSONIOPerformer.JSONIOPerformerJSONDecodeException;
import satir.framework.JSONIOPerformer.JSONIOPerformerReadException;
import satir.framework.refectoring.Factory;
import satir.framework.refectoring.Factory.FactoryException;

/**
 * The Class LazyInitiator.
 * @version 3.0 April 2012
 * @author Creative Family
 */
public final class LazyInitiator {

    /** Factory. */
    private static Hashtable<String, Factory> factories = new Hashtable<String, Factory>();

    /** Registry. */
    private static Hashtable<String, Hashtable<String, Object>> registry = new Hashtable<String, Hashtable<String, Object>>();

    /**
     * Instantiates a new lazy initiator.
     */
    private LazyInitiator() {

    }

    /**
     * Return an object by given ClassType and Key referring to .config file,
     * and create if necessary.
     * @param classType
     *            the class type
     * @param key
     *            the key
     * @return the object
     * @since 2.0
     */
    public static Object get(final String classType, final String key) {
	return get(classType, key, false);
    }

    /**
     * Return an object by given ClassType and Key referring to .config file.
     * @param classType
     *            - field "class" in Satirgame.config
     * @param key
     *            - field "key" in Satirgame.config
     * @param isNewInstance
     *            indicate "Must create new instance" or "Create if necessary"
     * @return the object
     * @since 2.0
     */
    public static Object get(final String classType, final String key, final boolean isNewInstance) {

	// Early return for retrieving
	if (!isNewInstance && contains(key, classType)) {
	    return pop(key, classType);
	}

	try {
	    // Read Game configuration
	    JSONIOPerformer satirGameConfigIO;
	    satirGameConfigIO = new JSONIOPerformer("config/SatirGame.config");
	    JSONObject satirGameConfig = satirGameConfigIO.retrieve();
	    JSONArray implementations = satirGameConfig
		    .getJSONArray("implementation");

	    // Retrieve implementation details
	    Hashtable<String, Object> implementation = getImplementation(
		    classType, implementations);
	    if (implementation.isEmpty()) {
		throw new LazyInitiatorImplementationNotFoundException(
			classType);
	    }
	    // Retrieve essential details
	    JSONObject factory = (JSONObject) implementation.get("factory");

	    // Default config
	    JSONObject configJSONObject = new JSONObject();
	    configJSONObject.put("key", key);
	    configJSONObject.put("class", classType);

	    // Retrieve config (Optional)
	    if (implementation.containsKey("config")) {

		JSONObject config = (JSONObject) implementation.get("config");

		// Read instantiation configuration
		String configFullpath = "";
		if (!config.getString("directory").isEmpty()) {
		    configFullpath += config.getString("directory");
		}
		configFullpath += config.getString("filename");
		String root = config.getString("root");

		// Retrieve instantiation details
		configJSONObject = getConfig(key, root, configFullpath, null);
	    }

	    // Get factory
	    Factory factoryInstantiation = getFactory(factory);

	    // New Instantiation
	    Object instantiation = factoryInstantiation
		    .create(configJSONObject);

	    // Store instantiation
	    if (!isNewInstance) {
		put(key, classType, instantiation);
	    }
	    return instantiation;

	} catch (JSONIOPerformerReadException e) {
	    // TODO Auto-generated catch block
	    e.printStackTrace();
	    System.out.println(e.getMessage());
	    System.exit(0);
	    return null;
	} catch (JSONIOPerformerJSONDecodeException e) {
	    // TODO Auto-generated catch block
	    e.printStackTrace();
	    System.out.println(e.getMessage());
	    System.exit(0);
	    return null;
	} catch (JSONException e) {
	    // TODO Auto-generated catch block
	    e.printStackTrace();
	    System.out.println(e.getMessage());
	    System.exit(0);
	    return null;
	} catch (LazyInitiatorImplementationNotFoundException e) {
	    // TODO Auto-generated catch block
	    e.printStackTrace();
	    System.out.println(e.getMessage());
	    System.exit(0);
	    return null;
	} catch (LazyInitiatorFactoryNotFoundException e) {
	    // TODO Auto-generated catch block
	    e.printStackTrace();
	    System.out.println(e.getMessage());
	    System.exit(0);
	    return null;
	} catch (LazyInitiatorConfigNotFoundException e) {
	    // TODO Auto-generated catch block
	    e.printStackTrace();
	    System.out.println(e.getMessage());
	    System.exit(0);
	    return null;
	} catch (FactoryException e) {
	    // TODO Auto-generated catch block
	    e.printStackTrace();
	    System.out.println(e.getMessage());
	    System.exit(0);
	    return null;
	}
    }

    /**
     * Gets the factory.
     * @param config
     *            the config
     * @return the factory
     * @throws LazyInitiatorFactoryNotFoundException
     *             the lazy initiator factory not found exception "factory" : {
     *             "key" : "CommandFactory", "class" : "CommandFactory",
     *             "package" : "satir.command.factory", }
     */
    protected static Factory getFactory(final JSONObject config)
	    throws LazyInitiatorFactoryNotFoundException {
	String key = "", className = "", packageName = "";
	try {
	    key = config.getString("key");
	} catch (JSONException e) {
	    throw new LazyInitiatorFactoryNotFoundException(key, className,
		    packageName);
	}
	if (factories.containsKey(key)) {
	    return factories.get(key);
	} else {
	    try {
		packageName = config.getString("package");
	    } catch (JSONException e2) {
		e2.printStackTrace();
	    }
	    try {
		className = config.getString("class");
		String classPath = "";
		if (!packageName.isEmpty()) {
		    classPath = packageName + '.';
		}
		classPath += className;
		Factory instantiation = (Factory) Class.forName(classPath)
			.newInstance();
		return instantiation;
	    } catch (JSONException e2) {
		throw new LazyInitiatorFactoryNotFoundException(key, className,
			packageName);
	    } catch (InstantiationException e1) {
		throw new LazyInitiatorFactoryNotFoundException(key, className,
			packageName);
	    } catch (IllegalAccessException e1) {
		throw new LazyInitiatorFactoryNotFoundException(key, className,
			packageName);
	    } catch (ClassNotFoundException e1) {
		throw new LazyInitiatorFactoryNotFoundException(key, className,
			packageName);
	    }
	}
    }

    /**
     * Retrieve all configuration for specific implementation matching with
     * given key.
     * @param key
     *            the key
     * @param implementations
     *            the implementations
     * @return Hashtable<String, Object>
     * @since 2.0
     */
    protected static Hashtable<String, Object> getImplementation(final String key,
	    final JSONArray implementations) {
	Hashtable<String, Object> implementation = new Hashtable<String, Object>();
	for (int i = 0; i < implementations.length(); i++) {
	    try {
		JSONObject node = implementations.getJSONObject(i);
		String nodeKey = node.getString("key");
		if (nodeKey.equals(key)) {
		    // Retrieve all fields
		    String[] fields = JSONObject.getNames(node);
		    for (int j = 0; j < fields.length; j++) {
			String field = fields[j];
			implementation.put(field, node.get(field));
		    }
		    break;
		} else if (node.has("implementation")) {
		    // Search child
		    implementation = getImplementation(key,
			    node.getJSONArray("implementation"));
		    if (!implementation.isEmpty()) {
			// Add parent fields
			String[] fields = JSONObject.getNames(node);
			for (int j = 0; j < fields.length; j++) {
			    String field = fields[j];
			    // Add if no conflict
			    if (!field.equals("implementation")
				    && !implementation.containsKey(field)) {
				implementation.put(field, node.get(field));
			    }
			}
			break;
		    }
		}
	    } catch (JSONException e) {
		continue;
	    }
	}
	return implementation;
    }

    /**
     * Gets the config.
     * @param key
     *            the key
     * @param root
     *            the root
     * @param configFilePath
     *            the config file path
     * @param listKey
     *            the list key
     * @return the config
     * @throws LazyInitiatorConfigNotFoundException
     *             the lazy initiator config not found exception
     */
    protected static JSONObject getConfig(final String key, final String root,
	    final String configFilePath, final String listKey)
	    throws LazyInitiatorConfigNotFoundException {
	try {
	    JSONIOPerformer instantiationIOPerformer = new JSONIOPerformer(
		    configFilePath);
	    JSONObject instantiationRoot = instantiationIOPerformer.retrieve();
	    JSONArray configList = instantiationRoot.getJSONArray(root);
	    JSONObject config = getConfig(key, configList, listKey);
	    if (config.length() == 0) {
		throw new LazyInitiatorConfigNotFoundException(key,
			configFilePath, root);
	    }
	    return config;
	} catch (JSONException e) {
	    throw new LazyInitiatorConfigNotFoundException(key, configFilePath,
		    root);
	} catch (JSONIOPerformerReadException e) {
	    throw new LazyInitiatorConfigNotFoundException(key, configFilePath,
		    root);
	} catch (JSONIOPerformerJSONDecodeException e) {
	    throw new LazyInitiatorConfigNotFoundException(key, configFilePath,
		    root);
	}
    }

    /**
     * Retrieve a config by given key, configList and listKey.
     * @param key
     *            - the identifier
     * @param configList
     *            - the list of configuration
     * @param listKey
     *            - the field name of list
     * @return the config
     * @since 2.0
     */
    private static JSONObject getConfig(final String key, final JSONArray configList,
	    final String listKey) {
	JSONObject config = new JSONObject();
	for (int i = 0; i < configList.length(); i++) {
	    try {
		JSONObject node = configList.getJSONObject(i);
		String nodeKey = node.getString("key");
		if (nodeKey.equals(key)) {
		    config = node;
		    break;
		} else if (listKey != null && node.has(listKey)) {
		    config = getConfig(key, node.getJSONArray(listKey), listKey);
		}
	    } catch (JSONException e) {
		continue;
	    }
	}
	return config;
    }

    /**
     * Contains.
     * @param key
     *            the key
     * @param classType
     *            the class type
     * @return true, if successful
     */
    private static boolean contains(final String key, final String classType) {
	if (registry.containsKey(classType)) {
	    Hashtable<String, Object> classRegistry = registry.get(classType);
	    return classRegistry.containsKey(key);
	} else {
	    return false;
	}
    }

    /**
     * Put.
     * @param key
     *            the key
     * @param classType
     *            the class type
     * @param instantiation
     *            the instantiation
     */
    private static void put(final String key, final String classType, final Object instantiation) {
	if (!registry.containsKey(classType)) {
	    registry.put(classType, new Hashtable<String, Object>());
	}
	Hashtable<String, Object> classRegistry = registry.get(classType);
	classRegistry.put(key, instantiation);
    }

    /**
     * Pop.
     * @param key
     *            the key
     * @param classType
     *            the class type
     * @return the object
     */
    private static Object pop(final String key, final String classType) {
	Hashtable<String, Object> classRegistry = registry.get(classType);
	return classRegistry.get(key);
    }

    /**
     * The Class LazyInitiatorException.
     */
    public static class LazyInitiatorException extends Exception {

	/** serial version UID. */
	private static final long serialVersionUID = -6800928511480519625L;

	/**
	 * Instantiates a new lazy initiator exception.
	 * @param message
	 *            the message
	 */
	public LazyInitiatorException(final String message) {
	    super(message);
	}
    }

    /**
     * The Class LazyInitiatorImplementationNotFoundException.
     */
    public static class LazyInitiatorImplementationNotFoundException extends
	    LazyInitiatorException {

	/** serial version UID. */
	private static final long serialVersionUID = -7075125228823400146L;

	/**
	 * Instantiates a new lazy initiator implementation not found exception.
	 * @param key
	 *            the key
	 */
	public LazyInitiatorImplementationNotFoundException(final String key) {
	    super(
		    "ERROR: The required componment not found in this verion. Reference: key ["
			    + key + "]");
	}
    }

    /**
     * The Class LazyInitiatorFactoryNotFoundException.
     */
    public static class LazyInitiatorFactoryNotFoundException extends
	    LazyInitiatorException {

	/** serial version UID. */
	private static final long serialVersionUID = 3482188311054025790L;

	/**
	 * Instantiates a new lazy initiator factory not found exception.
	 * @param key
	 *            the key
	 * @param className
	 *            the class name
	 * @param packageName
	 *            the package name
	 */
	public LazyInitiatorFactoryNotFoundException(final String key,
		final String className, final String packageName) {
	    super(
		    "ERROR: The required factory not found in this verion. Reference: key ["
			    + key + "], class [" + className + "], package ["
			    + packageName + "]");
	}
    }

    /**
     * The Class LazyInitiatorConfigNotFoundException.
     */
    public static class LazyInitiatorConfigNotFoundException extends
	    LazyInitiatorException {

	/** serial version UID. */
	private static final long serialVersionUID = -1217526605554037853L;

	/**
	 * Instantiates a new lazy initiator config not found exception.
	 * @param key
	 *            the key
	 * @param configPath
	 *            the config path
	 * @param root
	 *            the root
	 */
	public LazyInitiatorConfigNotFoundException(final String key,
		final String configPath, final String root) {
	    super(
		    "ERROR: The required config not found in the config file. Reference: key ["
			    + key + "], config file [" + configPath
			    + "], tree root [" + root + "]");
	}
    }
}
